import {
  GM_deleteValue,
  GM_listValues,
  GM_getValue,
  GM_setValue,
  GM_registerMenuCommand,
  GM_unregisterMenuCommand,
  GM_addStyle,
  GM_getResourceText,
  GM_xmlhttpRequest,
  GM_download,
  GM_info,
  GM,
  Types,
  unsafeWindow,
} from "tm";
import { debug, features, speedOnly } from "./buildConfig.mts";
import preset from "./_presets.mjs";
import { logger } from "./logger.mts";
import { data } from "./globalData.mts";
import type {
  BRule,
  AbpObject,
  CssObject,
  ETagObject,
  MenuItem,
  AnyFunction,
} from "./types.d.mts";

export const defaultValues = {
  get black() {
    return "";
  },
  get rules(): AbpObject {
    return {};
  },
  get css(): CssObject {
    return {
      needUpdate: true,
      genHideCss: "",
      genExtraCss: "",
      spcHideCss: "",
      spcExtraCss: "",
    };
  },
  get time() {
    return "0/0/0 0:0:0";
  },
  get etags(): ETagObject {
    return {};
  },
  get brules(): BRule[] {
    return [];
  },
  get hash() {
    return "";
  },
};

export const values: {
    /**
     * ajs_disabled_domains 禁用拦截的域名 features.menu
     * @param value 域名数组，(null) 删除，() get
     */
    black(value?: string[] | null): Promise<string[] | undefined>;
    /**
     * ajs_saved_abprules 存储的规则 features.fromValues
     * @param value 存储规则对象，(null) 删除，() get
     */
    rules(value?: AbpObject | null): Promise<AbpObject | undefined>;
    /**
     * ajs_saved_styles_* 预存的 CSS features.presave
     * @param value 预存对象，(null) 删除，(void 0) get
     * @param host 域名，默认为当前访问域名
     */
    css(
      value?: CssObject | null,
      host?: string,
    ): Promise<CssObject | undefined>;
    /**
     * ajs_rules_ver 规则更新日期 features.update
     * @param value `Date.toLocaleString("zh-CN")`，(null) 删除，() get
     */
    time(value?: string | null): Promise<string | undefined>;
    /**
     * ajs_rules_etags 订阅规则的 ETag features.update
     * @param value 订阅规则 ETag 对象，(null) 删除，() get
     */
    etags(value?: ETagObject | null): Promise<ETagObject | undefined>;
    /**
     * ajs_modifier_rules 排除修饰符规则 features.basicException
     * @param value BRule 对象数组，(null) 删除，() get
     */
    brules(value?: BRule[] | null): Promise<BRule[] | undefined>;
    /**
     * ajs_custom_hash 自定义规则 SHA-1 features.customHash
     * @param value `Uint32Array.toString()`，(null) 删除，() get
     */
    hash(value?: string | null): Promise<string | undefined>;
  } = {
    async black(value?: string[] | null) {
      if (value === void 0) {
        const arrayString = await gmValue(
          "get",
          false,
          "ajs_disabled_domains",
          defaultValues.black,
        );
        return typeof arrayString == "string" && arrayString.length > 0
          ? arrayString.split(",")
          : [];
      } else {
        await gmValue(
          "set",
          false,
          "ajs_disabled_domains",
          value === null ? null : value.join(","),
        );
        return void 0;
      }
    },
    async rules(value?: AbpObject | null) {
      return value === void 0
        ? await gmValue("get", true, "ajs_saved_abprules", defaultValues.rules)
        : await gmValue("set", true, "ajs_saved_abprules", value);
    },
    async css(value?: CssObject | null, host: string = location.hostname) {
      return value === void 0
        ? await gmValue(
            "get",
            true,
            `ajs_saved_styles_${host}`,
            defaultValues.css,
          )
        : await gmValue("set", true, `ajs_saved_styles_${host}`, value);
    },
    async time(value?: string | null) {
      return value === void 0
        ? await gmValue("get", false, "ajs_rules_ver", defaultValues.time)
        : await gmValue("set", false, "ajs_rules_ver", value);
    },
    async etags(value?: ETagObject | null) {
      return value === void 0
        ? await gmValue("get", true, "ajs_rules_etags", defaultValues.etags)
        : await gmValue("set", true, "ajs_rules_etags", value);
    },
    async brules(value?: BRule[] | null) {
      return value === void 0
        ? await gmValue("get", true, "ajs_modifier_rules", defaultValues.brules)
        : await gmValue("set", true, "ajs_modifier_rules", value);
    },
    async hash(value?: string | null) {
      return value === void 0
        ? await gmValue("get", false, "ajs_custom_hash", defaultValues.hash)
        : await gmValue("set", false, "ajs_custom_hash", value);
    },
  } as const,
  menus: Record<string, MenuItem> = {
    /** 禁用拦截菜单 */
    disable: {
      id: void 0,
      text() {
        return Promise.resolve(
          `在此域名${data.disabled ? "启用" : "禁用"}拦截`,
        );
      },
    },
    /** 更新规则菜单 */
    update: {
      id: void 0,
      async text(): Promise<string> {
        const time = (await values.time()) ?? "0";
        return data.updating
          ? "正在更新..."
          : `点击更新 ${time.startsWith("0") ? "未知时间" : time}`;
      },
    },
    /** 清空规则菜单 */
    count: {
      id: void 0,
      async text(): Promise<string> {
        const abpRules = (await values.rules()) ?? defaultValues.rules;
        let ruleCount = 0;
        for (const rules of Object.values(abpRules)) {
          ruleCount += rules.split("\n").length;
        }
        const cssCounts = await getCssLength();
        return data.isClean
          ? `已清空，点击${data.disabled ? "刷新网页" : "重新加载规则"}`
          : `点击清空，规则 ${String(ruleCount)} 预存 ${cssCounts.join(",")}`;
      },
    },
    /** 导出报告菜单 */
    export: {
      id: void 0,
      text() {
        let cssCount = "";
        if (!data.disabled) {
          if ((data.appliedLevel & 1) == 0) {
            cssCount += data.genHideCss + data.genExtraCss;
          }
          if ((data.appliedLevel & 2) == 0) {
            cssCount += data.spcHideCss + data.spcExtraCss;
          }
        }
        return Promise.resolve(
          `下载统计报告 ${
            data.saved
              ? `CSS ${String(cssCount.split("*/").length - 1)}`
              : `规则 ${String(data.appliedCount)} / ${String(
                  data.allRules.split("\n").length,
                )}`
          }`,
        );
      },
    },
  } as const;

/**
 * 选择合适的 油猴/模拟 接口
 * @param {(Function | undefined)} gm1 GM_xxx
 * @param {(Function | undefined)} gm4 GM.xxx
 * @returns {(Function | undefined)} 合适的接口 或者 undefined
 */
function gmChooser<T extends AnyFunction, U extends AnyFunction>(
  gm1: T | undefined,
  gm4: U | undefined,
): T | U | undefined {
  const gm1dm = gm1?.name.startsWith("DM_");
  const gm4dm = gm4?.name.startsWith("DM_");
  if (gm1 && gm4) {
    if (gm1dm === gm4dm) {
      return gm1;
    } else {
      return gm1dm ? gm4 : gm1;
    }
  } else {
    return gm1 ?? gm4 ?? void 0;
  }
}

/**
 * 添加/删除/替换 油猴脚本菜单项
 * @async
 * @param {string} name 菜单项的 key
 * @param {Function} cb 点击菜单项回调，不指定即删除菜单项
 * @returns {Promise.<void>}
 */
export async function gmMenu(
  name: keyof typeof menus,
  callback?: Parameters<typeof GM_registerMenuCommand>[1],
): Promise<void> {
  const id = menus[name].id ?? void 0;
  const gmr = gmChooser(GM_registerMenuCommand, GM.registerMenuCommand);
  const gmu = gmChooser(GM_unregisterMenuCommand, GM.unregisterMenuCommand);
  if (typeof gmr != "function" || data.isFrame) return;
  if (id !== void 0 && typeof gmu == "function") {
    debug && !speedOnly && logger("color", `删除菜单 ${name}`, "red");
    menus[name].id = void 0;
    await gmu(id);
  }
  if (typeof callback == "function") {
    debug && !speedOnly && logger("color", `添加菜单 ${name}`, "green");
    menus[name].id = await gmr(await menus[name].text(), callback);
  }
  debug && !speedOnly && console.info(name, id, menus[name].id);
  debug &&
    !speedOnly &&
    logger("data", {
      菜单: "ID",
      disable: menus.disable.id,
      update: menus.update.id,
      count: menus.count.id,
      export: menus.export.id,
    });
  return;
}

/**
 * 油猴 Value 接口封装
 * @async
 * @param {string} action get 读
 * @param {boolean} json 解析 JSON
 * @param {string} key 键名
 * @param {*} [defaultValue] 默认值
 * @returns {Promise.<*>} 值
 */
export async function gmValue<T>(
  action: "get",
  json: boolean,
  key: string,
  defaultValue?: T,
): Promise<T>;
/**
 * 油猴 Value 接口封装
 * @async
 * @param {string} action set 写
 * @param {boolean} json 存储为 JSON
 * @param {string} key 键名
 * @param {*} [value] 存储值，null 或留空删除
 * @returns {Promise.<void>}
 */
export async function gmValue<T>(
  action: "set",
  json: boolean,
  key: string,
  value?: T | null,
): Promise<undefined>;
export async function gmValue<T>(
  action: "get" | "set",
  json: boolean,
  key: string,
  value?: T,
): Promise<T | undefined> {
  switch (action) {
    case "get": {
      try {
        let v = gmChooser(GM_getValue, GM.getValue)?.(
          key,
          json ? JSON.stringify(value) : value,
        );
        v = v instanceof Promise ? await v : v;
        return json && typeof v == "string" ? (JSON.parse(v) as T) : (v as T);
      } catch (error) {
        debug &&
          !speedOnly &&
          logger("error", "GM_getValue", (error as Error).message);
        return value;
      }
    }
    case "set": {
      try {
        return value === null || value === void 0
          ? void gmChooser(GM_deleteValue, GM.deleteValue)?.(key)
          : void gmChooser(GM_setValue, GM.setValue)?.(
              key,
              json ? JSON.stringify(value) : value,
            );
      } catch (error) {
        debug &&
          !speedOnly &&
          logger("error", "GM_setValue", (error as Error).message);
        return void gmChooser(GM_deleteValue, GM.deleteValue)?.(key);
      }
    }
  }
}

/**
 * 获取脚本猫用户配置，非脚本猫返回默认值
 * @async
 * @param {string} prop 用户配置项 key
 * @returns {Promise.<*>} 用户配置项的值
 */
export async function getUserConfig<T extends keyof typeof preset.userConfig>(
  property: T,
): Promise<(typeof preset.userConfig)[T]> {
  return features.userConfig
    ? ((await gmValue<(typeof preset.userConfig)[T]>(
        "get",
        false,
        `配置.${property}`,
      )) ?? preset.userConfig[property])
    : preset.userConfig[property];
}

/**
 * 可靠的向页面注入 CSS，失败自动重试
 * @async
 * @param {string} css 需要注入的 CSS
 * @param {number} [pass=0] 当前重试次数，留空
 * @returns {Promise.<void>} 返回空的 Promise
 */
export async function addStyle(css: string, pass = 0): Promise<void> {
  if (pass >= data.tryCount) return;
  const element = await gmChooser(GM_addStyle, GM.addStyle)?.(css);
  debug && !speedOnly && logger("info", "<style>", element, pass);
  if (
    typeof element == "object" &&
    !document.documentElement.contains(element)
  ) {
    globalThis.setTimeout(() => {
      void addStyle(css, pass + 1);
    }, data.tryTimeout);
  }
  return;
}

/**
 * 异步 GM_xmlhttpRequest 封装
 * @async
 * @param details GM_xmlhttpRequest 的 details
 * @returns 返回 Promise
 *
 * 成功 resolve GM_xmlhttpRequest onload Response
 *
 * 失败 reject 如下对象
 * ```ts
 * type XhrError = {
 *   error: "noxhr" | "abort" | "error" | "timeout" | "Via timeout";
 *   resp?: Response;
 * }
 * ```
 */
export async function promiseXhr(
  details: Omit<Types.Xhr.Details, `on${string}`>,
): Promise<Types.Xhr.Response | undefined> {
  const timeout = details.method === "HEAD" ? data.headTimeout : data.timeout;
  let loaded = false;
  const gmXhr = gmChooser(GM_xmlhttpRequest, GM.xmlHttpRequest);
  if (typeof gmXhr != "function") {
    throw {
      error: "noxhr",
    };
  }
  debug && !speedOnly && logger("info", "XHR 配置", details);
  return await new Promise<Types.Xhr.Response>((resolve, reject) => {
    gmXhr({
      onload(event_) {
        loaded = true;
        resolve(event_);
      },
      onabort(event_) {
        loaded = true;
        reject({
          error: "abort",
          resp: event_,
        });
      },
      onerror(event_) {
        loaded = true;
        reject({
          error: "error",
          resp: event_,
        });
      },
      ontimeout(event_) {
        loaded = true;
        reject({
          error: "timeout",
          resp: event_,
        });
      },
      onreadystatechange(event_) {
        // Via 浏览器超时中断，不给成功状态...
        if (event_.readyState === 3) {
          setTimeout(() => {
            if (!loaded) {
              reject({
                error: "Via timeout",
                resp: event_,
              });
            }
          }, timeout);
        }
      },
      timeout,
      ...details,
    });
  });
}

/**
 * GM_getResourceText 代理
 * @async
 * @param {string} key `@resource` 的 key
 * @returns {(?string | undefined)} GM_getResourceText 的返回，不支持的返回 undefined
 */
export async function getRuleFromResource(
  key: string,
): Promise<ReturnType<typeof GM_getResourceText> | undefined> {
  try {
    return await gmChooser(GM_getResourceText, GM.getResourceText)?.(key);
  } catch (error) {
    debug &&
      !speedOnly &&
      logger("error", "GM_getResourceText", (error as Error).message);
    return null;
  }
}

/**
 * 清空脚本使用的所有 Value
 */
export async function cleanValue() {
  const gmdv = gmChooser(GM_deleteValue, GM.deleteValue);
  const saves = await getSavedHosts();
  for (const save of saves) {
    await gmdv?.(`ajs_saved_styles_${save}`);
  }
  await gmdv?.("ajs_disabled_domains");
  await gmdv?.("ajs_saved_abprules");
  await gmdv?.("ajs_rules_etags");
  await gmdv?.("ajs_rules_ver");
  await gmdv?.("ajs_hasSave_domains");
  await gmdv?.("ajs_modifier_rules");
  await gmdv?.("ajs_custom_hash");
  return;
}

/**
 * 打印油猴接口可用情况
 */
export function gmReport() {
  logger("data", {
    GM_getValue:
      typeof GM_getValue == "function"
        ? GM_getValue.name.startsWith("DM_")
          ? "DM"
          : "OK"
        : "NO",
    GM_setValue:
      typeof GM_setValue == "function"
        ? GM_setValue.name.startsWith("DM_")
          ? "DM"
          : "OK"
        : "NO",
    GM_listValues:
      typeof GM_listValues == "function"
        ? GM_listValues.name.startsWith("DM_")
          ? "DM"
          : "OK"
        : "NO",
    GM_deleteValue:
      typeof GM_deleteValue == "function"
        ? GM_deleteValue.name.startsWith("DM_")
          ? "DM"
          : "OK"
        : "NO",
    "GM.getValue": typeof GM.getValue == "function" ? "OK" : "NO",
    "GM.setValue": typeof GM.setValue == "function" ? "OK" : "NO",
    "GM.listValues": typeof GM.listValues == "function" ? "OK" : "NO",
    "GM.deleteValue": typeof GM.deleteValue == "function" ? "OK" : "NO",
    GM_registerMenuCommand:
      typeof GM_registerMenuCommand == "function" ? "OK" : "NO",
    GM_unregisterMenuCommand:
      typeof GM_unregisterMenuCommand == "function" ? "OK" : "NO",
    GM_xmlhttpRequest: typeof GM_xmlhttpRequest == "function" ? "OK" : "NO",
    GM_download:
      typeof GM_download == "function"
        ? GM_download.name.startsWith("DM_")
          ? "DM"
          : "OK"
        : "NO",
    GM_getResourceText: typeof GM_getResourceText == "function" ? "OK" : "NO",
    GM_addStyle:
      typeof GM_addStyle == "function"
        ? GM_addStyle.name.startsWith("DM_")
          ? "DM"
          : "OK"
        : "NO",
  });
  logger("info", "GM_info", GM_info);
  logger("info", "GM", GM);
}

/**
 * 保证只运行一次
 * @async
 * @param {string} key 互斥字符串
 * @param {Function} func 运行函数
 * @returns {Promise.<*>} Promise，重复运行或失败 reject
 */
export async function runOnce<T>(
  key: string,
  function_: () => T | Promise<T>,
): Promise<Awaited<T>> {
  if (key in unsafeWindow) {
    return await Promise.reject(new Error(key));
  }
  unsafeWindow[key] = true;
  const result = await function_();
  return result;
}

const OnErrorLogger: OnErrorEventHandlerNonNull = (
  error,
  source,
  line,
  col,
) => {
  logger("error", [error, source, line, col].join("\n"));
};

/**
 * 监听并打印脚本运行错误
 */
export function errorListen() {
  globalThis.onerror = OnErrorLogger;
  unsafeWindow.onerror = OnErrorLogger;
}

/**
 * GM_download 代理
 * @param {string} url 下载 url
 * @param {string} name 文件名
 */
export function downUrl(url: string, name: string) {
  GM_download({
    url,
    name,
  });
}

/**
 * 检查域名是否有预存
 * @async
 * @param {string} host 域名
 * @returns {Promise.<boolean>} 域名是否有预存
 */
export async function getSavedHosts(host: string): Promise<boolean>;
/**
 * 获取有预存的域名
 * @async
 * @returns {Promise.<string[]>} 有预存的域名数组
 */
export async function getSavedHosts(): Promise<string[]>;
/**
 * 检查域名是否有预存，不指定域名则返回有预存的域名数组
 * @async
 * @param {?string} host 域名
 * @returns {Promise.<(boolean | string[])>} 域名是否有预存 / 有预存的域名数组
 */
export async function getSavedHosts(
  host?: string,
): Promise<boolean | string[]> {
  const keys = (await gmChooser(GM_listValues, GM.listValues)?.()) ?? [];
  debug && !speedOnly && logger("color", "values", "royalblue", keys);
  const domains = (
    Array.isArray(keys)
      ? keys
      : // Rains
        Object.keys(keys as Record<string, unknown>)
  )
    .filter((key) => key.startsWith("ajs_saved_styles_"))
    .map((key) => key.replace("ajs_saved_styles_", ""));
  return host ? domains.includes(host) : domains;
}

/**
 * 获取篡改猴脚本注释
 * @returns {string} 脚本注释，不支持返回空字符串
 */
export function getComment(): string {
  return GM_info.script.options?.comment ?? "";
}

/**
 * 获取预存域名数量和预存总大小
 * @async
 * @returns {Promise.<number[]>} [预存域名数量, 预存总大小]
 */
export async function getCssLength(): Promise<[number, number]> {
  const savedHosts = await getSavedHosts();
  let savedChars = 0;
  for (const savedHost of savedHosts) {
    const co = (await values.css(void 0, savedHost))!;
    savedChars += JSON.stringify(co).length;
  }
  return [savedHosts.length, savedChars];
}
